package com.gastudio.gadownloading;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorGroup;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.render.*;
import ohos.agp.utils.Color;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.agp.window.service.Display;
import ohos.agp.window.service.DisplayAttributes;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * author: GAStudio
 * qq:1935400187
 * 技术交流qq群:277582728
 */
public class GADownloadingView extends ComponentContainer implements Component.DrawTask,
    Component.EstimateSizeListener, ComponentContainer.ArrangeListener {
  private static final int ZERO_PROGRESS = 0;
  private static final int FULL_PROGRESS = 100;
  private static final int HALF_PROGRESS = 50;
  private static final int INVALID_PROGRESS = -1;
  private static final float FULL_NORMALIZED_TIME = 1F;

  private static final float FULL_NORMALIZED_PROGRESS = 1F;
  private static final float HALF_NORMALIZED_PROGRESS = 0.5F;
  private static final String FULL_PROGRESS_STR = "100%";
  private static final String FULL_PROGRESS_DONE_STR = "done";
  private static final String FAILED_PROGRESS_STR = "failed";

  private static final int FULL_ALPHA = 255;
  private static final int FULL_ANGLE = 360;
  private static final int HALF_FULL_ANGLE = FULL_ANGLE / 2;

  private static final int DEFAULT_ARROW_COLOR = 0xFFFFFFFF;
  private static final int DEFAULT_LOADING_CIRCLE_BG_COLOR = 0xFF491C14;
  private static final int DEFAULT_LOADING_LINE_COLOR = 0xFF491c14;
  private static final int DEFAULT_PROGRESS_LINE_LEFT_COLOR = 0XFFFFFFFF;
  private static final int DEFAULT_PROGRESS_TEXT_COLOR = 0xFF000000;
  private static final int DEFAULT_DONE_PROGRESS_TEXT_COLOR = 0xFF5F9C62;

  private static final int BEFORE_PROGRESS_CIRCLE_SCALE_DURATION = 450;
  private static final int BEFORE_PROGRESS_INNER_CIRCLE_SCALE_DURATION
      = BEFORE_PROGRESS_CIRCLE_SCALE_DURATION / 2;
  private static final int BEFORE_PROGRESS_INNER_CIRCLE_SCALE_DELAY
      = BEFORE_PROGRESS_CIRCLE_SCALE_DURATION - BEFORE_PROGRESS_INNER_CIRCLE_SCALE_DURATION;
  private static final int BEFORE_PROGRESS_CIRCLE_TO_LINE_DURATION = 150;
  private static final int BEFORE_PROGRESS_ARROW_MOVE_AND_LINE_OSCILL = 800;

  private static final float[] CIRCLE_TO_LINE_SEASONS = new float[]{0, 0.4f, 0.8f, 1f};
  private static final float[] CIRCLE_TO_LINE_WIDTH_FACTOR = new float[]{1f, 1.2f, 2.4f, 3.45f};
  private static final float[] CIRCLE_TO_LINE_HEIGHT_FACTOR = new float[]{1f, 0.73f, 0.36f, 0f};
  private static final float[] CIRCLE_TO_LINE_FST_CON_X_FACTOR = new float[]{-0.65f, 0.18f, 0.72f
      , 1.04f};
  private static final float[] CIRCLE_TO_LINE_FST_CON_Y_FACTOR = new float[]{0f, 0.036f, 0f, 0f};
  private static final float[] CIRCLE_TO_LINE_SEC_CON_X_FACTOR = new float[]{-0.65f, 0.06f, 0.72f
      , 1.04f};
  private static final float[] CIRCLE_TO_LINE_SEC_CON_Y_FACTOR = new float[]{1f, 0.73f, 0.36f, 0f};
  // value of MAX_LINE_WIDTH_FACTOR is max value in CIRCLE_TO_LINE_WIDTH_FACTOR array
  public static final float MAX_LINE_WIDTH_FACTOR = 3.45f;

  private static final int FULL_PROGRESS_ANIMATION_DURATION = 3000;
  private static final int MIN_PROGRESS_ANIMATION_DURATION = 100;

  private static final int DONE_ANIMATION_DURATION = 500;
  private static final int DONE_LINE_PACK_UP_ANIMATION_DURATION = 500;
  private static final int DONE_LINE_PACK_UP_ARROW_SHAKE_ANIMATION_DURATION = 500;
  private static final int DONE_REST_TO_CIRCLE_DOWN_ANIMATION_DURATION = 1000;
  private static final int DONE_DIALOG_TO_ARROW_DURATION
      = DONE_REST_TO_CIRCLE_DOWN_ANIMATION_DURATION / 4;
  private static final int DONE_DIALOG_UP_DOWN_DURATION
      = DONE_REST_TO_CIRCLE_DOWN_ANIMATION_DURATION - DONE_DIALOG_TO_ARROW_DURATION;


  private static final int DONE_LINE_PACK_UP_ARROW_SHAKE_BASE_POINT_DIAMETER = 2;
  private static final int FAILED_ANIMATION_DURATION = 1000;
  private static final int FAILED_BOMB_ANIMATION_DURATION =
      FAILED_ANIMATION_DURATION / 3;
  private static final int FAILED_ROPE_OSCILLATION_ANIMATION_DURATION = 250;
  private static final int FAILED_ARROW_MOVE_ANIMATION_DURATION = 800;
  private static final int FAILED_ROPE_PACK_UP_ANIMATION_DURATION =
      (FAILED_ARROW_MOVE_ANIMATION_DURATION - FAILED_ROPE_OSCILLATION_ANIMATION_DURATION) / 2;
  private static final int FAILED_CIRCLE_SCALE_ANIMATION_DURATION =
      (FAILED_ARROW_MOVE_ANIMATION_DURATION - FAILED_ROPE_OSCILLATION_ANIMATION_DURATION) / 2;

  // state of arrow's rectangle and triangle
  private static final float DEFAULT_INIT_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO = 0.5F;
  private static final float DEFAULT_INIT_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO = 0.5F;
  private static final float DEFAULT_INIT_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO = 1F;
  private static final float DEFAULT_INIT_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO = 0.5F;
  private static final float DEFAULT_MIDDLE_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO = 0.7F;
  private static final float DEFAULT_END_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO = 1F;
  private static final float DEFAULT_END_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO = 0.5F;
  private static final float DEFAULT_END_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO = 0.34F;
  private static final float DEFAULT_END_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO = 0.17F;
  private static final float DEFAULT_ARROW_TOP_CONNER_RADIUS_TO_CIRCLE_RADIUS_RATIO = 0.05f;

  private static final float DEFAULT_ARROW_MOVE_MAX_HEIGHT_TO_CIRCLE_DIAMETER_RATIO =
      MAX_LINE_WIDTH_FACTOR / 2
      + (DEFAULT_INIT_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO
      + DEFAULT_INIT_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO) / 4;

  private static final float SUGGEST_X_AXIS_PADDING_TO_CIRCLE_DIAMETER_RATIO = 0.75F;
  private static final int SUGGEST_LOADING_VIEW_WIDTH = 300;
  private static final int SUGGEST_LOADING_VIEW_HEIGHT = (int) (SUGGEST_LOADING_VIEW_WIDTH
      / (SUGGEST_X_AXIS_PADDING_TO_CIRCLE_DIAMETER_RATIO + MAX_LINE_WIDTH_FACTOR)
      * DEFAULT_ARROW_MOVE_MAX_HEIGHT_TO_CIRCLE_DIAMETER_RATIO);

  // progress text size and line stroke width
  private static final int DEFAULT_PROGRESS_TEXT_SIZE = 12;
  private static final int MIN_PROGRESS_TEXT_SIZE = 8;
  private static final float PROGRESS_TEXT_SIZE_TO_VIEW_WIDTH_RATIO
      = (float) DEFAULT_PROGRESS_TEXT_SIZE / SUGGEST_LOADING_VIEW_WIDTH;
  private static final int DEFAULT_LOADING_LINE_STROKE_WIDTH = 3;
  private static final int MIN_LOADING_LINE_STROKE_WIDTH = 1;
  private static final float LOADING_LINE_STROKE_WIDTH_TO_VIEW_WIDTH_RATIO
      = (float) DEFAULT_LOADING_LINE_STROKE_WIDTH / SUGGEST_LOADING_VIEW_WIDTH;

  private static final float CHANGE_ARROW_TO_DIALOG_RECT_CHANGE = 0.563F;
  private static final float CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME = 0.625F;
  private static final float ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_1 = 0.313F;
  private static final float ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_2 = 0.626F;
  private static final float ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_3 = 0.813F;
  private static final float ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_4 = 1F;

  private static final int ROTATE_ARROW_TO_DIALOG_INIT_ANGLE = 0;
  private static final int ROTATE_ARROW_TO_DIALOG_ANGLE_1 = -30;
  private static final int ROTATE_ARROW_TO_DIALOG_ANGLE_2 = 20;
  private static final int ROTATE_ARROW_TO_DIALOG_ANGLE_3 = -10;

  private static final float DEFAULT_PROGRESS_MAX_HEIGHT_TO_BASELINE_RATIO = 0.1F;
  private static final float DEFAULT_LINE_OSCILLATION_MAX_HEIGHT_TO_BASELINE_RATIO = 0.15F;
  private static final float ARROW_BOUNCE_LENGTH_RATIO = 0.2F;
  private static final float ARROW_BOUNCE_LENGTH_RATIO_2 = 0.1F;

  // Animation State
  private static final int STATE_BEFORE_PROGRESS_CIRCLE_SCALE = 1;
  private static final int STATE_BEFORE_PROGRESS_INNER_CIRCLE_SCALE = 2;
  private static final int STATE_BEFORE_PROGRESS_CIRCLE_TO_LINE = 3;
  private static final int STATE_BEFORE_PROGRESS_ARROW_MOVE_LINE_OSCILL = 4;

  private static final int STATE_IN_PROGRESS = 5;
  private static final int STATE_DONE_ROTATE = 6;
  private static final int STATE_DONE_LINE_PACK_UP = 7;
  private static final int STATE_DONE_ARROW_SHAKE = 8;
  private static final int STATE_DONE_REST_TO_CIRCLE = 9;

  private static final int STATE_FAILED_ARROW_SHAKE = 10;
  private static final int STATE_FAILED_LINE_OSCILL = 11;
  private static final int STATE_FAILED_LINE_PACK_UP = 12;
  private static final int STATE_FAILED_CIRCLE_SCALE = 13;

  private static final int DONE_LINE_PACK_UP_ARROW_ANGLE = 10;

  // other
  private Paint mDefaultPaint;
  //    private Xf mXfermode;
  private ThreeDimView mCamera;
  private List<Animator> mAnimatorList;

  // loadingView
  private int mLoadingViewCenterX;
  private int mLoadingViewCenterY;
  private int mLoadingViewWidth;
  private int mLoadingViewHeight;

  // arrow
  private Path mArrowPath;
  private RectFloat mArrowRectF;
  private int mInitArrowRectWidth;
  private int mInitArrowRectHeight;
  private int mInitArrowTriWidth;
  private int mInitArrowTriHeight;
  private int mInitArrowJointConnerRadius;
  private int mEndArrowRectHeight;
  private int mLastArrowRectWidth;
  private int mLastArrowRectHeight;
  private int mLastArrowTriWidth;
  private int mLastArrowTriHeight;
  private int mLastArrowOffsetX;
  private int mLastArrowOffsetY;
  private float[] mChangeArrowToDialogParamters = new float[5];
  private Matrix mArrowRotateMatrix;

  // arrow move
  private Matrix mArrowMovePoint;
  private Path mArrowMovePath;
  private Rect mArrowMovePathRect;
  private PathMeasure mArrowPathMeasure;
  private float mArrowMovePathLength;

  // arrow bounce
  private Path mArrowBouncePath;
  private PathMeasure mArrowBouncePathMeasure;
  private float mArrowBouncePathLength;

  // circle
  private RectFloat mCircleRectF;
  private int mCircleRadius;
  private int mCircleDiameter;

  // line
  private Path mOscillationLinePath;
  private Paint mBaseLinePaint;
  private Path mBaseLinePath;
  private RectFloat mBaseLineRectF;
  private int mBaseLineLen;
  private int mHalfBaseLineLen;
  private int mBaseLineStrokeWidth;
  private int mBaseLineX;
  private int mBaseLineY;
  private int mBaseLineCenterX;
  private int mProgressLineMaxHeight;
  private int mLineOscillationMaxHeight;

  // progress text
  private Paint mProgressTextPaint;
  private Rect mProgressTextRect;
  private int mProgressTextSize;

  // before progress
  private AnimatorGroup mBefProgressAnimatorSet;
  private AnimatorValue mBefProgCircleScaleAnimator;
  private float mBefProgCircleScalingFactor;
  private AnimatorValue mBefProgInnerCircleScaleAnimator;
  private float mBefProgInnerCircleScalingFactor;
  private AnimatorValue mBefProgCircleToLineAnimator;
  private float mBefProgCircleToLineNormalizedTime;
  private AnimatorValue mBefProgArrowMoveAnimator;
  private float mBefProgArrowMoveNormalizedTime;
  private AnimatorValue mBefProgLineOscillAnimator;
  private float mBefProgLineOscillationFactor;

  // in progress
  private float mProgressValue;
  private int mLastProgress;
  private int mNextProgress;
  private AnimatorValue mProgressAnimator;
  private RectFloat mProgressLinePathRectF;
  private int mLastValidProgress;
  private String mLastValidProgressTextStr;

  // done
  private AnimatorGroup mDoneAnimatorSet;

  private AnimatorValue mDoneRotateAnimator;
  private float mDoneRotateNormalizedTime;

  private AnimatorValue mDoneLinePackUpAnimator;
  private float mDoneLinePackUpNormalizedTime;

  private AnimatorValue mDoneArrowShakeAnimator;
  private float mDoneArrowShakeAngle;

  private AnimatorValue mDoneRestToCircleAnimator;
  private float mDoneRestToCircleScalingFactor;

  private AnimatorValue mDoneDialogToArrowAnimator;
  private float mDoneDialogToArrowNormalizedTime;

  private AnimatorValue mDoneDialogToArrowUpDownAnimator;
  private float mDoneDialogToArrowUpDownFactor;

  //fail
  private boolean mIsFailed;
  private AnimatorGroup mFailedAnimatorSet;

  private AnimatorValue mFailedArrowShakeAnimator;
  private float mFailedArrowUpAndDownFactor;
  private AnimatorValue mFailedArrowRotateAnimator;
  private float mFailedArrowRotateAngle;

  private AnimatorValue mFailedLineOscillationAnimator;
  private float mFailedLineOscillationFactor;

  private AnimatorValue mFailedArrowMoveAnimator;
  private float mFailedArrowMoveNormalizeTiem;

  private AnimatorValue mFailedLinePackUpAnimator;
  private float mFailedLinePackUpFactor;

  private AnimatorValue mFailedCircleScaleAnimator;
  private float mFailedCircleScaleFactor;

  private AnimatorValue mFailedBombAnimator;
  private float mFailedBombAnimatorPer;
  private Paint mFailedBombPaint;
  private Paint mFailedBombBellowPaint;
  private Path mFailedBombPath;
  private Path mFailedBombPathBellow;

  private int mCurrentState;
  private Color mArrowColor;
  private Color mLoadingCircleBackColor;
  private Color mLoadingLineColor;
  private Color mProgressLineColor;
  private Color mProgressTextColor;
  private Color mDoneTextColor;

  private float[] data;

  public GADownloadingView(Context context) {
    this(context, null);
  }

  public GADownloadingView(Context context, AttrSet attrs) {
    this(context, attrs, 0);
  }

  public GADownloadingView(Context context, AttrSet attrs, int defStyleAttr) {
    super(context, attrs, String.valueOf(defStyleAttr));
    mArrowColor = attrs.getAttr("arrow_color").isPresent() ?
        attrs.getAttr("arrow_color").get().getColorValue() : new Color(DEFAULT_ARROW_COLOR);
    mLoadingCircleBackColor = attrs.getAttr("loading_circle_back_color").isPresent() ?
        attrs.getAttr("loading_circle_back_color").get().getColorValue() :
        new Color(DEFAULT_LOADING_CIRCLE_BG_COLOR);
    mLoadingLineColor = attrs.getAttr("loading_line_color").isPresent() ? attrs.getAttr(
        "loading_line_color").get().getColorValue() : new Color(DEFAULT_LOADING_LINE_COLOR);
    mProgressLineColor = attrs.getAttr("progress_line_color").isPresent() ? attrs.getAttr(
        "progress_line_color").get().getColorValue() : new Color(DEFAULT_PROGRESS_LINE_LEFT_COLOR);
    mProgressTextColor = attrs.getAttr("progress_text_color").isPresent() ? attrs.getAttr(
        "progress_text_color").get().getColorValue() : new Color(DEFAULT_PROGRESS_TEXT_COLOR);
    mDoneTextColor = attrs.getAttr("done_text_color").isPresent() ? attrs.getAttr(
        "done_text_color").get().getColorValue() : new Color(DEFAULT_DONE_PROGRESS_TEXT_COLOR);

    init();
  }

  private void init() {
    mDefaultPaint = new Paint();
    mDefaultPaint.setAntiAlias(true);
//        mXfermode = new PopupDialog();

    mBaseLinePaint = new Paint();
    mBaseLinePaint.setAntiAlias(true);
    mBaseLinePaint.setStyle(Paint.Style.STROKE_STYLE);
    mBaseLinePaint.setStrokeCap(Paint.StrokeCap.BUTT_CAP);
    mBaseLinePaint.setPathEffect(new PathEffect(10f));
    mBaseLinePath = new Path();
    mBaseLineRectF = new RectFloat();

    mArrowPath = new Path();
    mCircleRectF = new RectFloat();
    mArrowRectF = new RectFloat();

    mIsFailed = false;
    mNextProgress = ZERO_PROGRESS;
    mLastValidProgressTextStr = "";
    mLastValidProgress = INVALID_PROGRESS;
    mProgressTextPaint = new Paint();
    mProgressTextPaint.setColor(mProgressTextColor);
    mProgressTextPaint.setAntiAlias(true);
    mProgressTextRect = new Rect();

    mLoadingViewWidth = dipToPx(getContext(), SUGGEST_LOADING_VIEW_WIDTH);
    mLoadingViewHeight = dipToPx(getContext(), SUGGEST_LOADING_VIEW_HEIGHT);
    mAnimatorList = new ArrayList<Animator>();

    setEstimateSizeListener(this);
    setArrangeListener(this);
    addDrawTask(this);
  }

  private void updateArrowPath(int rectWith, int rectHeight, int triWidth, int triHeight) {
    if (mArrowPath == null) {
      mArrowPath = new Path();
    } else if (mLastArrowRectWidth == rectWith && mLastArrowRectHeight == rectHeight
        && mLastArrowTriWidth == triWidth && mLastArrowTriHeight == triHeight) {
      return;
    } else {
      mArrowPath.reset();
    }
    mLastArrowRectWidth = rectWith;
    mLastArrowRectHeight = rectHeight;
    mLastArrowTriWidth = triWidth;
    mLastArrowTriHeight = triHeight;

    int arrowWidth = Math.max(rectWith, triWidth);
    int halfArrowWidth = arrowWidth / 2;
    int arrowHeight = rectHeight + triHeight;
    int rectPaddingLeft = (arrowWidth - rectWith) / 2;
    int triPaddingLeft = (arrowWidth - triWidth) / 2;

    // move to bottom center
    mArrowPath.moveTo(halfArrowWidth, 0);
    // rect bottom left edge
    mArrowPath.lineTo(rectPaddingLeft, 0);
    // rect left edge
    mArrowPath.lineTo(rectPaddingLeft, rectHeight);
    // tri bottom left edge
    mArrowPath.lineTo(triPaddingLeft, rectHeight);
    // tri left edge
    mArrowPath.lineTo(halfArrowWidth, arrowHeight);
    // tri right edge
    mArrowPath.lineTo(arrowWidth - triPaddingLeft, rectHeight);
    // tri bottom right edge
    mArrowPath.lineTo(arrowWidth - rectPaddingLeft, rectHeight);
    // rect right edge
    mArrowPath.lineTo(arrowWidth - rectPaddingLeft, 0);
    // rect right bottom edge
    mArrowPath.lineTo(halfArrowWidth, 0);

    // update path RectF
    if (mArrowRectF == null) {
      mArrowRectF = new RectFloat();
    }
    mArrowPath.computeBounds(mArrowRectF);
  }

  @Override
  public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSpecMode = EstimateSpec.getMode(widthMeasureSpec);
    int heightSpecMode = EstimateSpec.getMode(heightMeasureSpec);
    if (widthSpecMode != EstimateSpec.ESTIMATED_STATE_BIT_MASK) {
      widthMeasureSpec =
          EstimateSpec.getSizeWithMode(mLoadingViewWidth, EstimateSpec.PRECISE);
    }
    if (heightSpecMode != EstimateSpec.ESTIMATED_STATE_BIT_MASK) {
      heightMeasureSpec =
          EstimateSpec.getSizeWithMode(mLoadingViewHeight * 2, EstimateSpec.PRECISE);
    }
    setEstimatedSize(widthMeasureSpec, heightMeasureSpec);
    return true;
  }

  @Override
  public boolean onArrange(int left, int top, int widthMeasureSpec, int heightMeasureSpec) {
    mLoadingViewCenterX = widthMeasureSpec / 2;
    mLoadingViewCenterY = heightMeasureSpec / 2;
    mCircleDiameter = (int) Math.min(widthMeasureSpec / (MAX_LINE_WIDTH_FACTOR
            + SUGGEST_X_AXIS_PADDING_TO_CIRCLE_DIAMETER_RATIO),
        heightMeasureSpec / (DEFAULT_ARROW_MOVE_MAX_HEIGHT_TO_CIRCLE_DIAMETER_RATIO));
    int actualViewWidth = (int) (mCircleDiameter * MAX_LINE_WIDTH_FACTOR
        + SUGGEST_X_AXIS_PADDING_TO_CIRCLE_DIAMETER_RATIO);
    mCircleRadius = mCircleDiameter / 2;

    mCircleRectF.modify(0, 0, mCircleDiameter, mCircleDiameter);
    mCircleRectF.translateTo((widthMeasureSpec - mCircleRectF.getWidth()) / 2,
        (heightMeasureSpec - mCircleRectF.getHeight()) / 2);
    mInitArrowRectWidth = (int) (mCircleRadius
        * DEFAULT_INIT_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO);
    mInitArrowRectHeight = (int) (mCircleRadius
        * DEFAULT_INIT_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO);
    mInitArrowTriWidth = (int) (mCircleRadius
        * DEFAULT_INIT_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO);
    mInitArrowTriHeight = (int) (mCircleRadius
        * DEFAULT_INIT_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO);
    mInitArrowJointConnerRadius = (int) (mCircleRadius
        * DEFAULT_ARROW_TOP_CONNER_RADIUS_TO_CIRCLE_RADIUS_RATIO);
    mEndArrowRectHeight = (int) (mCircleRadius
        * DEFAULT_END_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO);
    mArrowRectF.modify(mCircleRectF);
    mArrowRectF.translate(
        (mCircleRectF.getWidth() - Math.max(mInitArrowRectWidth, mInitArrowTriWidth)) / 2,
        (mCircleRectF.getHeight() - mInitArrowRectHeight - mInitArrowTriHeight) / 2);

    mBaseLineStrokeWidth = dipToPx(getContext(), (int) Math.max(MIN_LOADING_LINE_STROKE_WIDTH,
        LOADING_LINE_STROKE_WIDTH_TO_VIEW_WIDTH_RATIO * actualViewWidth / getScreenDensity(getContext())));
    mBaseLinePaint.setStrokeWidth(mBaseLineStrokeWidth);
    mBaseLineLen = (int) (mCircleDiameter * MAX_LINE_WIDTH_FACTOR);
    mHalfBaseLineLen = mBaseLineLen / 2;
    mBaseLineX = mLoadingViewCenterX - mHalfBaseLineLen;
    mBaseLineY = mLoadingViewCenterY - mBaseLineStrokeWidth / 2;
    mBaseLineCenterX = mLoadingViewCenterX;
    mProgressLineMaxHeight =
        (int) (mBaseLineLen * DEFAULT_PROGRESS_MAX_HEIGHT_TO_BASELINE_RATIO);
    mLineOscillationMaxHeight =
        (int) (mBaseLineLen * DEFAULT_LINE_OSCILLATION_MAX_HEIGHT_TO_BASELINE_RATIO);

    mProgressTextSize = dipToPx(getContext(), (int) Math.max(MIN_PROGRESS_TEXT_SIZE,
        PROGRESS_TEXT_SIZE_TO_VIEW_WIDTH_RATIO * actualViewWidth / getScreenDensity(getContext())));
    mProgressTextPaint.setTextSize(mProgressTextSize);

    mDefaultPaint.setPathEffect(null);
    return true;
  }

  @Override
  public void onDraw(Component component, Canvas canvas) {
    switch (mCurrentState) {
      // in this state, scale circle and arrow
      case STATE_BEFORE_PROGRESS_CIRCLE_SCALE:
        drawCircleAndArrowScale(canvas, mBefProgCircleScalingFactor);
        break;
      // in this state, scale circle, arrow and inner circle
      case STATE_BEFORE_PROGRESS_INNER_CIRCLE_SCALE:
        drawCircleAndArrowScaleAndInnerScale(canvas, mBefProgCircleScalingFactor,
            mBefProgInnerCircleScalingFactor);
        break;
      // in this state, circle change to a line, while the arrow moves up along the y-axis
      case STATE_BEFORE_PROGRESS_CIRCLE_TO_LINE:
        drawBeforeProgressCircleToLine(canvas, mBefProgCircleToLineNormalizedTime);
        break;
      // in this state, arrow move to left, while line oscillate
      case STATE_BEFORE_PROGRESS_ARROW_MOVE_LINE_OSCILL:
        if (mBefProgArrowMoveNormalizedTime == 1.0f) {
          mBefProgArrowMoveNormalizedTime = 0.0f;
        }
        drawBeforeProgressArrowMoveAndLineOscill(canvas,
            mBefProgArrowMoveNormalizedTime, mBefProgLineOscillationFactor);
        break;
      // in this state, draw arrow move by progress
      case STATE_IN_PROGRESS:
        drawProgress(canvas, mProgressValue);
        break;
      // in this state, arrow is rotate by y
      case STATE_DONE_ROTATE:
        drawArrowRotate(canvas, mDoneRotateNormalizedTime);
        break;

      // in this state, arrow move to center, line pack up
      case STATE_DONE_LINE_PACK_UP:
        drawDoneLinePackUp(canvas, mDoneLinePackUpNormalizedTime);
        break;
      // in this state, arrow shake
      case STATE_DONE_ARROW_SHAKE:
        drawDoneLinePackUpArrowShake(canvas, mDoneArrowShakeAngle);
        break;
      case STATE_DONE_REST_TO_CIRCLE:
        drawDoneRestToCircle(canvas, mDoneRestToCircleScalingFactor,
            mDoneDialogToArrowNormalizedTime, mDoneDialogToArrowUpDownFactor);
        mProgressValue = 0.0f;
        break;
      case STATE_FAILED_ARROW_SHAKE:
        drawFailedArrowShake(canvas, mFailedArrowUpAndDownFactor, mFailedArrowRotateAngle, 15f);
        break;
      case STATE_FAILED_LINE_OSCILL:
        drawFailedRopeOscillation(canvas, mFailedLineOscillationFactor);
        drawFailedArrowMove(canvas, mFailedArrowMoveNormalizeTiem);
        break;
      case STATE_FAILED_LINE_PACK_UP:
        drawFailedLinePackUp(canvas, mFailedLinePackUpFactor);
        drawFailedArrowMove(canvas, mFailedArrowMoveNormalizeTiem);
        break;
      case STATE_FAILED_CIRCLE_SCALE:
        drawFailedCircleScale(canvas, mFailedCircleScaleFactor);
        drawFailedArrowMove(canvas, mFailedArrowMoveNormalizeTiem);
        mProgressValue = 0.0f;
        break;
      default:
        break;
    }
  }

  private void drawBombPoint(Canvas canvas, float normalizedTime, int lastProgress,
                             int highestPointHeight) {
    if (mFailedBombPaint == null || mFailedBombBellowPaint == null) {
      Path circle = new Path();
      // generate bomb point shape
      circle.addCircle(0, 0, mBaseLineStrokeWidth / 2, Path.Direction.COUNTER_CLOCK_WISE);
      circle.addCircle(mBaseLineStrokeWidth, 0, mBaseLineStrokeWidth / 3,
          Path.Direction.COUNTER_CLOCK_WISE);
      circle.addCircle(mBaseLineStrokeWidth * 2, 0, mBaseLineStrokeWidth / 4,
          Path.Direction.COUNTER_CLOCK_WISE);
      circle.addCircle(mBaseLineStrokeWidth * 3, 0, mBaseLineStrokeWidth / 5,
          Path.Direction.COUNTER_CLOCK_WISE);
      mFailedBombPaint = new Paint();
      mFailedBombPaint.setStrokeWidth(mBaseLineStrokeWidth);
      mFailedBombPaint.setAntiAlias(true);
      mFailedBombPaint.setColor(mProgressLineColor);
      mFailedBombPaint.setStyle(Paint.Style.STROKE_STYLE);

      mFailedBombPaint.setPathEffect(new PathEffect(circle,
          mBaseLineStrokeWidth * 3, 0, PathEffect.Style.TRANSLATE_STYLE));
      mFailedBombBellowPaint = new Paint(mFailedBombPaint);
      mFailedBombBellowPaint.setPathEffect(new PathEffect(circle,
          mBaseLineStrokeWidth * 3, HALF_FULL_ANGLE, PathEffect.Style.TRANSLATE_STYLE));
    }

    if (mFailedBombPath == null || mFailedBombPathBellow == null) {
      mFailedBombPath = new Path();
      float normalizeProgress = (float) lastProgress / FULL_PROGRESS;
      int baseLineEndX = (int) (mBaseLineX + normalizeProgress * mBaseLineLen);
      int baseLineEndY;
      float k = (float) highestPointHeight / mHalfBaseLineLen;
      if (normalizeProgress < HALF_NORMALIZED_PROGRESS) {
        baseLineEndY =
            (int) (mHalfBaseLineLen * k * normalizeProgress / HALF_NORMALIZED_PROGRESS) + mBaseLineY;
      } else {
        baseLineEndY =
            (int) (mHalfBaseLineLen * k * (1 - normalizeProgress) / HALF_NORMALIZED_PROGRESS) + mBaseLineY;
      }
      mFailedBombPath.moveTo(mBaseLineX, mBaseLineY - mBaseLineStrokeWidth / 2);
      mFailedBombPath.lineTo(baseLineEndX, baseLineEndY);
      mFailedBombPathBellow = new Path(mFailedBombPath);
      mFailedBombPathBellow.offset(0, mBaseLineStrokeWidth / 2);
    }
    mFailedBombPaint.setAlpha((int) (FULL_ALPHA * (1 - normalizedTime)));
    mFailedBombBellowPaint.setAlpha((int) (FULL_ALPHA * (1 - normalizedTime)));
    canvas.save();
    canvas.translate(0, -mBaseLineStrokeWidth * normalizedTime);
    canvas.drawPath(mFailedBombPath, mFailedBombPaint);
    canvas.translate(0, mBaseLineStrokeWidth * 2 * normalizedTime);
    canvas.drawPath(mFailedBombPathBellow, mFailedBombBellowPaint);
    canvas.restore();
  }

  private void drawBeforeProgressArrowMoveAndLineOscill(Canvas canvas, float normalizeTime,
                                                        float lineOsscilFactor) {
    // update arrow paramters
    updateArrowToDialogParamters(mChangeArrowToDialogParamters, normalizeTime);
    // height point of path move
    int maxMovePathHeight =
        (int) (mCircleDiameter * DEFAULT_ARROW_MOVE_MAX_HEIGHT_TO_CIRCLE_DIAMETER_RATIO);

    updateArrowMovePoint(normalizeTime,
        CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME, mHalfBaseLineLen,
        maxMovePathHeight, mLoadingViewCenterX - mHalfBaseLineLen,
        mLoadingViewCenterY - maxMovePathHeight, false);
    // move the center of arrow by path
    mLastArrowOffsetX = (int) (data[2] - mArrowRectF.getWidth() / 2);
    mLastArrowOffsetY = (int) (data[5] - mArrowRectF.getHeight());
    drawArrowTrans(canvas, mLastArrowOffsetX, mLastArrowOffsetY, mChangeArrowToDialogParamters);
    int maxHeightPointOfLineOscill =
        (int) (mBaseLineLen * DEFAULT_LINE_OSCILLATION_MAX_HEIGHT_TO_BASELINE_RATIO);
    updateLineOscillationPath(lineOsscilFactor, mBaseLineLen,
        mBaseLineX, mBaseLineY, maxHeightPointOfLineOscill, mHalfBaseLineLen);
    canvas.drawPath(mOscillationLinePath, mBaseLinePaint);
  }

  private void drawBeforeProgressCircleToLine(Canvas canvas, float normalizeTime) {
    mBaseLinePaint.setColor(mLoadingLineColor);
    // update path in center of bounds[0, 0, mCircleDiameter, mCircleDiameter]
    updateCircleToLinePath(mBaseLinePath, mCircleDiameter, normalizeTime);
    // offset the path to the place of circle
    mBaseLinePath.offset(mCircleRectF.left, mCircleRectF.top);
    canvas.drawPath(mBaseLinePath, mBaseLinePaint);
    // compute bounds to get the lowest (center) point of the path
    mBaseLinePath.computeBounds(mBaseLineRectF);

    // because the bounds of arrow is always [0, 0, width, height]
    mLastArrowOffsetX =
        (int) (mCircleRectF.getCenter().getPointX() - mArrowRectF.getCenter().getPointX());
    mLastArrowOffsetY =
        (int) (mCircleRectF.getCenter().getPointY() - mArrowRectF.getCenter().getPointY());
    // calculate the bottom of arrow
    int arrowBottom = (int) (mLastArrowOffsetY + mArrowRectF.getHeight());
    // determine if the bottom of the rope is below the bottom of the arrow
    if (mBaseLineRectF.bottom <= arrowBottom) {
      // move arrow up
      mLastArrowOffsetY += (int) (mBaseLineRectF.bottom - arrowBottom);
    }
    drawArrowTrans(canvas, mLastArrowOffsetX, mLastArrowOffsetY, 0);
  }

  private void drawProgress(Canvas canvas, float progress) {
    float normalizedProgress = progress / FULL_PROGRESS;
    drawProgressLinePath(canvas, normalizedProgress, mBaseLineLen,
        mBaseLineX, mBaseLineY, mProgressLineMaxHeight, mProgressLineColor);
    mLastArrowOffsetX = (int) (mProgressLinePathRectF.left - mArrowRectF.getWidth() / 2
        + mBaseLineLen * normalizedProgress);
    mLastArrowOffsetY = (int) (mProgressLinePathRectF.bottom - mArrowRectF.getHeight());
    drawArrowTrans(canvas, mLastArrowOffsetX, mLastArrowOffsetY, 0);

    // draw progress text
    if (mLastValidProgress != (int) progress) {
      mLastValidProgressTextStr = String.valueOf((int) progress) + "%";
      mProgressTextPaint.getTextBounds(mLastValidProgressTextStr);
    }
    int offsetHeight = (mEndArrowRectHeight - mProgressTextRect.getHeight()) / 2;
    int textBaseLineX = (int) (mLastArrowOffsetX
        + (mArrowRectF.getWidth() - mProgressTextRect.getWidth()) / 4);
    int textBastLineY = mLastArrowOffsetY + offsetHeight - mProgressTextRect.top;
    mProgressTextPaint.setColor(mProgressTextColor);
    canvas.drawText(mProgressTextPaint, mLastValidProgressTextStr, textBaseLineX, textBastLineY);
  }

  private void drawArrowRotate(Canvas canvas, float normalizedTime) {
    if (mArrowRotateMatrix == null) {
      mArrowRotateMatrix = new Matrix();
    } else {
      mArrowRotateMatrix.reset();
    }
    mArrowRotateMatrix.reset();

    float angle;
    String tmpStr = mLastValidProgressTextStr;
    if (normalizedTime <= HALF_NORMALIZED_PROGRESS) {
      // first text is 100%, rotate angle is  0 to 90
      mLastValidProgressTextStr = FULL_PROGRESS_STR;
      angle = HALF_FULL_ANGLE * normalizedTime;
      mProgressTextPaint.setColor(mProgressTextColor);
    } else {
      // second text is done, rotate angle is  270 to 360
      mLastValidProgressTextStr = FULL_PROGRESS_DONE_STR;
      angle = HALF_FULL_ANGLE * normalizedTime + HALF_FULL_ANGLE;
      mProgressTextPaint.setColor(mDoneTextColor);
    }
//        if (mCamera == null) {
//            mCamera = new ThreeDimView();
//        }
//        mCamera.release();
//        mCamera.rotateY(angle);
//        mCamera.getMatrix(mArrowRotateMatrix);
//        mCamera.restore();
    mArrowRotateMatrix.preTranslate(-mArrowRectF.getCenter().getPointX(),
        -mArrowRectF.getCenter().getPointY());
    mArrowRotateMatrix.postTranslate(mArrowRectF.getCenter().getPointX(),
        mArrowRectF.getCenter().getPointY());

    mLastArrowOffsetX = (int) (mBaseLineX + mBaseLineLen - mArrowRectF.getWidth() / 2);
    mLastArrowOffsetY = (int) (mBaseLineY - mArrowRectF.getHeight());

    canvas.save();
    canvas.translate(mLastArrowOffsetX, mLastArrowOffsetY);
    canvas.concat(mArrowRotateMatrix);
    mDefaultPaint.setColor(mArrowColor);
    canvas.drawPath(mArrowPath, mDefaultPaint);

    // str is changed, need re-calculate bounds
    if (!tmpStr.equals(mLastValidProgressTextStr)) {
      mProgressTextPaint.getTextBounds(mLastValidProgressTextStr);
    }

    int textBaseLineX =
        (int) (mArrowRectF.left + (mArrowRectF.getWidth() - mProgressTextRect.getWidth()) / 4);
    int textBaseLineY = (int) (mArrowRectF.bottom - mArrowRectF.getHeight() / 2);
    canvas.drawText(mProgressTextPaint, mLastValidProgressTextStr, textBaseLineX, textBaseLineY);
    canvas.restore();

    // the FULL_NORMALIZED_TIME is means, no need bounce
    drawProgressLinePath(canvas, FULL_NORMALIZED_TIME, mBaseLineLen,
        mBaseLineX, mBaseLineY, mProgressLineMaxHeight, mProgressLineColor);
  }


  private void drawDoneLinePackUp(Canvas canvas, float normalizedTime) {
    int adjustLen = (int) (mBaseLineLen * (FULL_NORMALIZED_TIME - normalizedTime));
    // keep a min len
    if (adjustLen < DONE_LINE_PACK_UP_ARROW_SHAKE_BASE_POINT_DIAMETER) {
      adjustLen = DONE_LINE_PACK_UP_ARROW_SHAKE_BASE_POINT_DIAMETER;
    }
    int adjustBaseLineX = mBaseLineCenterX - adjustLen / 2;
    mLastArrowOffsetX = (int) (adjustBaseLineX + adjustLen - mArrowRectF.getWidth() / 2);
    mLastArrowOffsetY = (int) (mBaseLineY - mArrowRectF.getHeight());

    mBaseLinePaint.setColor(mProgressLineColor);
    canvas.drawLine(adjustBaseLineX, mBaseLineY, adjustBaseLineX + adjustLen, mBaseLineY,
        mBaseLinePaint);
    drawArrowTrans(canvas, mLastArrowOffsetX, mLastArrowOffsetY, DONE_LINE_PACK_UP_ARROW_ANGLE);
  }

  private void drawDoneLinePackUpArrowShake(Canvas canvas, float angle) {
    int adjustLen = DONE_LINE_PACK_UP_ARROW_SHAKE_BASE_POINT_DIAMETER;
    mBaseLinePaint.setColor(mProgressLineColor);
    canvas.drawLine(mBaseLineCenterX, mBaseLineY, mBaseLineCenterX + adjustLen, mBaseLineY,
        mBaseLinePaint);
    drawArrowTrans(canvas,
        (int) (mBaseLineCenterX - mArrowRectF.getWidth() / 2),
        (int) (mBaseLineY - mArrowRectF.getHeight()), angle);
  }

  private void drawDoneRestToCircle(Canvas canvas, float circleScaleFactor,
                                    float dialogToArrowNormalizeTime, float arrowUpDownFactor) {
    // draw bg circle
    float circleRadius = mCircleRadius * circleScaleFactor;
    mDefaultPaint.setColor(mLoadingCircleBackColor);
    canvas.drawCircle(mCircleRectF.getCenter().getPointX(), mCircleRectF.getCenter().getPointY(),
        circleRadius, mDefaultPaint);

    // draw arrow
    // generate dialog to arrow
    updateArrowToDialogParamters(mChangeArrowToDialogParamters, 1 - dialogToArrowNormalizeTime);
    updateArrowPath((int) (mCircleRadius * mChangeArrowToDialogParamters[0]),
        (int) (mCircleRadius * mChangeArrowToDialogParamters[1]),
        (int) (mCircleRadius * mChangeArrowToDialogParamters[2]),
        (int) (mCircleRadius * mChangeArrowToDialogParamters[3]));
    int offsetArrowX = (int) (mCircleRectF.getCenter().getPointX() - mArrowRectF.getWidth() / 2);
    // dialogToArrowNormalizeTime * 0.5 can ensure the arrow move end point smoothly
    int offsetArrowY = (int) (mCircleRectF.getCenter().getPointY() - mArrowRectF.getHeight()
        * (1 - dialogToArrowNormalizeTime * 0.5 + arrowUpDownFactor));
    canvas.save();
    canvas.translate(offsetArrowX, offsetArrowY);
    mDefaultPaint.setColor(mArrowColor);
    canvas.drawPath(mArrowPath, mDefaultPaint);
    canvas.restore();
  }

  private void drawFailedArrowShake(Canvas canvas, float upAndDownFactor, float rotateFactor,
                                    float maxRotateAngle) {
    float normalizedProgress = mProgressValue / FULL_PROGRESS;
    int offsetLine = (int) (mProgressLineMaxHeight * upAndDownFactor);
    int offsetArrow;
    if (normalizedProgress < HALF_NORMALIZED_PROGRESS) {
      offsetArrow = (int) (offsetLine * normalizedProgress / HALF_NORMALIZED_PROGRESS);
    } else {
      offsetArrow =
          (int) (offsetLine * (FULL_NORMALIZED_PROGRESS - normalizedProgress) / HALF_NORMALIZED_PROGRESS);
    }
    drawProgressLinePath(canvas, normalizedProgress, mBaseLineLen,
        mBaseLineX, mBaseLineY, mProgressLineMaxHeight + offsetLine, mLoadingLineColor);
    drawBombPoint(canvas, mFailedBombAnimatorPer, (int) mProgressValue, mProgressLineMaxHeight);

    if (!mLastValidProgressTextStr.equals(FAILED_PROGRESS_STR)) {
      mLastValidProgressTextStr = FAILED_PROGRESS_STR;
      mProgressTextPaint.getTextBounds(FAILED_PROGRESS_STR);
    }

    canvas.save();
    canvas.translate(mLastArrowOffsetX, mLastArrowOffsetY + offsetArrow);
    canvas.rotate(maxRotateAngle * rotateFactor, mArrowRectF.getCenter().getPointX(),
        mArrowRectF.bottom);
    canvas.save();
    canvas.rotate(HALF_FULL_ANGLE, mArrowRectF.getCenter().getPointX(), mArrowRectF.bottom);
    canvas.drawPath(mArrowPath, mDefaultPaint);
    canvas.restore();
    int textBaseLineX =
        (int) (mArrowRectF.left + (mArrowRectF.getWidth() - mProgressTextRect.getWidth()) / 4);
    int textBaseLineY = (int) (mArrowRectF.bottom + mArrowRectF.getHeight() +
        -(mEndArrowRectHeight - mProgressTextRect.getHeight()) / 2
        - mProgressTextRect.bottom);
    canvas.drawText(mProgressTextPaint, FAILED_PROGRESS_STR, textBaseLineX, textBaseLineY);
    canvas.restore();
  }

  private void drawFailedRopeOscillation(Canvas canvas, float ropeOsillFactor) {
    updateLineOscillationPath(ropeOsillFactor, mBaseLineLen,
        mBaseLineX, mBaseLineY, mLineOscillationMaxHeight, mHalfBaseLineLen);
    mBaseLinePaint.setColor(mLoadingLineColor);
    canvas.drawPath(mOscillationLinePath, mBaseLinePaint);
  }


  private void drawFailedArrowMove(Canvas canvas, float normalizedTime) {
    int circleRadius = (int) (mCircleRectF.getWidth() / 2);
    int halfLineLen = (int) (circleRadius * MAX_LINE_WIDTH_FACTOR);
    int startX = (int) (mLoadingViewCenterX
        + (mProgressValue - HALF_PROGRESS) / HALF_PROGRESS * halfLineLen);
    int width = Math.abs(startX - mLoadingViewCenterX);
    int maxMovePathHeight = (int) (circleRadius
        * DEFAULT_ARROW_MOVE_MAX_HEIGHT_TO_CIRCLE_DIAMETER_RATIO);

    updateArrowMovePoint(normalizedTime, FULL_NORMALIZED_TIME,
        width, maxMovePathHeight, Math.min(startX, mLoadingViewCenterX),
        mLoadingViewCenterY - maxMovePathHeight, mProgressValue < HALF_PROGRESS);
    int offsetArrowX = (int) (data[2] - mArrowRectF.getWidth() / 2);
    int offsetArrowY = (int) (data[5] - mArrowRectF.getHeight() / 2);

    updateFailedDialogToArrowParamters(mChangeArrowToDialogParamters, normalizedTime);
    updateArrowPath((int) (mCircleRadius * mChangeArrowToDialogParamters[0]),
        (int) (mCircleRadius * mChangeArrowToDialogParamters[1]),
        (int) (mCircleRadius * mChangeArrowToDialogParamters[2]),
        (int) (mCircleRadius * mChangeArrowToDialogParamters[3]));
    mDefaultPaint.setColor(mArrowColor);
    canvas.save();
    canvas.translate(offsetArrowX, offsetArrowY);
    canvas.concat(mArrowRotateMatrix);
    canvas.drawPath(mArrowPath, mDefaultPaint);
    canvas.restore();
  }

  private void drawFailedLinePackUp(Canvas canvas, float packUpFactor) {
    int halfLineLen = (int) (mHalfBaseLineLen * packUpFactor);
    mBaseLinePaint.setColor(mLoadingLineColor);
    canvas.drawLine(mLoadingViewCenterX - halfLineLen, mLoadingViewCenterY,
        mLoadingViewCenterX + halfLineLen, mLoadingViewCenterY, mBaseLinePaint);
  }

  private void drawFailedCircleScale(Canvas canvas, float scaleFactor) {
    int circleRadius = (int) (mCircleRadius * scaleFactor);
    mDefaultPaint.setColor(mLoadingCircleBackColor);
    canvas.drawCircle(mCircleRectF.getCenter().getPointX(), mCircleRectF.getCenter().getPointY(),
        circleRadius, mDefaultPaint);
  }

  private void updateArrowMovePoint(float normalizedTime, float startBounceTime,
                                    int width, int height, int left, int top, boolean isLTR) {
    if (mArrowMovePath == null) {
      mArrowMovePath = new Path();
    }

    if (mArrowPathMeasure == null) {
      mArrowPathMeasure = new PathMeasure(null, true);
    }

    if (mArrowBouncePath == null) {
      mArrowBouncePath = new Path();
    }

    if (mArrowBouncePathMeasure == null) {
      mArrowBouncePathMeasure = new PathMeasure(null, true);
    }

    if (mArrowMovePathRect == null) {
      mArrowMovePathRect = new Rect();
    }

    if (mArrowMovePathRect.getWidth() != width || mArrowMovePathRect.getHeight() != height
        || mArrowMovePathRect.left != left || mArrowMovePathRect.top != top) {
      mArrowMovePathRect.set(left, top, left + width, top + height);
      mArrowMovePath.reset();
      mArrowBouncePath.reset();

    }

    // move path
    if (mArrowMovePath.isEmpty()) {
      mArrowMovePath.moveTo(mArrowMovePathRect.left, mArrowMovePathRect.bottom);
      mArrowMovePath.cubicTo(mArrowMovePathRect.left + mArrowMovePathRect.getWidth() / 4,
          mArrowMovePathRect.top,
          mArrowMovePathRect.right,
          mArrowMovePathRect.top,
          mArrowMovePathRect.right, mArrowMovePathRect.bottom);
      mArrowPathMeasure.setPath(mArrowMovePath, false);
      mArrowMovePathLength = mArrowPathMeasure.getLength();
    }

    // bounce path
    if (mArrowBouncePath.isEmpty()) {
      mArrowBouncePath.moveTo(mArrowMovePathRect.right, mArrowMovePathRect.bottom);
      mArrowBouncePath.lineTo(mArrowMovePathRect.right, mArrowMovePathRect.bottom
          - mArrowRectF.getHeight() * ARROW_BOUNCE_LENGTH_RATIO);
      mArrowBouncePath.lineTo(mArrowMovePathRect.right, mArrowMovePathRect.bottom);
      mArrowBouncePath.lineTo(mArrowMovePathRect.right, mArrowMovePathRect.bottom
          - mArrowRectF.getHeight() * ARROW_BOUNCE_LENGTH_RATIO_2);
      mArrowBouncePath.lineTo(mArrowMovePathRect.right, mArrowMovePathRect.bottom);
      mArrowBouncePathMeasure.setPath(mArrowBouncePath, false);
      mArrowBouncePathLength = mArrowBouncePathMeasure.getLength();
    }

    if (mArrowMovePoint == null) {
      mArrowMovePoint = new Matrix();
    }

    // move
    if (normalizedTime <= startBounceTime) {
      mArrowPathMeasure.getMatrix(mArrowMovePathLength
          * normalizedTime / startBounceTime, mArrowMovePoint,
          PathMeasure.GET_POSITION_MATRIX_FLAG);
    } else {
      // bounce
      mArrowBouncePathMeasure.getMatrix(mArrowBouncePathLength
          * (normalizedTime - startBounceTime)
          / (FULL_NORMALIZED_TIME - startBounceTime), mArrowMovePoint,
          PathMeasure.GET_POSITION_MATRIX_FLAG);
    }
    if (!isLTR) {
      data = mArrowMovePoint.getData();
      data[2] = mArrowMovePathRect.getCenterX()
          - (data[2] - mArrowMovePathRect.getCenterX());
    }
  }

  /**
   * generate arrow change to dialog paramters
   *
   * @param paramters must be length = 5
   *                  paramteArr[0] and paramteArr[1] indicate arrow rectangle's width and
   *                  height,respectively.
   *                  paramteArr[2] and paramteArr[3] indicate arrow triangle's width and height,
   *                  respectively.
   *                  paramteArr[4] indicates arrow rotate angle.
   */
  private void updateArrowToDialogParamters(float[] paramters, float normalizeTime) {
    if (paramters == null || paramters.length != 5) {
      return;
    }

    // rect width
    if (normalizeTime <= CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME) {
      paramters[0] = (DEFAULT_END_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO
          - DEFAULT_INIT_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO)
          * normalizeTime / CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME
          + DEFAULT_INIT_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO;
    } else {
      paramters[0] = DEFAULT_END_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO;
    }

    // rect height
    if (normalizeTime <= CHANGE_ARROW_TO_DIALOG_RECT_CHANGE) {
      paramters[1] = (DEFAULT_MIDDLE_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO
          - DEFAULT_INIT_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO)
          * normalizeTime / CHANGE_ARROW_TO_DIALOG_RECT_CHANGE
          + DEFAULT_INIT_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO;
    } else if (normalizeTime <= CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME) {
      paramters[1] = (DEFAULT_END_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO
          - DEFAULT_MIDDLE_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO)
          * (normalizeTime - CHANGE_ARROW_TO_DIALOG_RECT_CHANGE)
          / (CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME - CHANGE_ARROW_TO_DIALOG_RECT_CHANGE)
          + DEFAULT_MIDDLE_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO;
    } else {
      paramters[1] = DEFAULT_END_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO;
    }

    // tri width and height
    if (normalizeTime < CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME) {
      // tri width
      paramters[2] = (DEFAULT_END_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO
          - DEFAULT_INIT_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO)
          * normalizeTime / CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME
          + DEFAULT_INIT_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO;
      // tri height
      paramters[3] = (DEFAULT_END_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO
          - DEFAULT_INIT_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO)
          * normalizeTime / CHANGE_ARROW_TO_DIALOG_BOUNCE_TIME
          + DEFAULT_INIT_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO;
    } else {
      // tri width
      paramters[2] = DEFAULT_END_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO;
      // tri height
      paramters[3] = DEFAULT_END_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO;
    }

    // angle
    if (normalizeTime <= ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_1) {
      paramters[4] = (ROTATE_ARROW_TO_DIALOG_ANGLE_1 - ROTATE_ARROW_TO_DIALOG_INIT_ANGLE)
          * normalizeTime / ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_1
          + ROTATE_ARROW_TO_DIALOG_INIT_ANGLE;
    } else if (normalizeTime <= ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_2) {
      paramters[4] = (ROTATE_ARROW_TO_DIALOG_ANGLE_2 - ROTATE_ARROW_TO_DIALOG_ANGLE_1)
          * (normalizeTime - ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_1)
          / (ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_2 - ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_1)
          + ROTATE_ARROW_TO_DIALOG_ANGLE_1;
    } else if (normalizeTime <= ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_3) {
      paramters[4] = (ROTATE_ARROW_TO_DIALOG_ANGLE_3 - ROTATE_ARROW_TO_DIALOG_ANGLE_2)
          * (normalizeTime - ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_2)
          / (ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_3 - ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_2)
          + ROTATE_ARROW_TO_DIALOG_ANGLE_2;
    } else {
      paramters[4] = (ROTATE_ARROW_TO_DIALOG_INIT_ANGLE - ROTATE_ARROW_TO_DIALOG_ANGLE_3)
          * (normalizeTime - ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_3)
          / (ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_4 - ROTATE_ARROW_TO_DIALOG_ANGLE_SEASON_3)
          + ROTATE_ARROW_TO_DIALOG_ANGLE_3;
    }

  }


  private void updateFailedDialogToArrowParamters(float[] paramters, float normalizeTime) {

    if (paramters == null || paramters.length != 5) {
      return;
    }

    // rect width
    paramters[0] = (DEFAULT_INIT_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO
        - DEFAULT_END_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO)
        * normalizeTime + DEFAULT_END_ARROW_RECT_WIDTH_TO_CIRCLE_RADIUS_RATIO;

    // rect height
    paramters[1] = (DEFAULT_INIT_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO
        - DEFAULT_END_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO)
        * normalizeTime + DEFAULT_END_ARROW_RECT_HEIGHT_TO_CIRCLE_RADIUS_RATIO;

    // tri width and height
    // tri width
    paramters[2] = (DEFAULT_INIT_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO
        - DEFAULT_END_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO)
        * normalizeTime + DEFAULT_END_ARROW_TRI_WIDTH_TO_CIRCLE_RADIUS_RATIO;
    // tri height
    paramters[3] = (DEFAULT_INIT_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO
        - DEFAULT_END_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO)
        * normalizeTime + DEFAULT_END_ARROW_TRI_HEIGHT_TO_CIRCLE_RADIUS_RATIO;

  }

  /**
   * Assumed the circle height is h, width is w, we can obtain the following content:
   * this animation have four State:
   * <p>
   * State 1: Circle to special line 1
   * width: w -> 1.2w
   * height: w -> 0.73w
   * firstConXFactor: -0.65w to 0.18w
   * firstConYFactor: 0 to 0.036w
   * SecondConXFactor: -0.65w to 0.06w
   * SecondConYFactor: 1w to 0.73w
   * <p>
   * State 2: line 1 to line 2
   * width: 1.2w -> 2.4w
   * height: w -> 0.36w
   * firstConXFactor: 0.18w to 0.72w
   * firstConYFactor: 0.036w to 0
   * SecondConXFactor: 0.06w to 0.72w
   * SecondConYFactor: 0.73w to 0.36w
   * <p>
   * State 3: line 2 to line 3
   * width: 2.4w -> 2.4w
   * height: 0.36w -> 0
   * firstConXFactor: 0.72w to 1.04w
   * firstConYFactor: 0 to  0
   * SecondConXFactor: 0.72w to 1.04w
   * SecondConYFactor: 0.36w to 0
   */

  private void updateCircleToLinePath(Path linePath, int circleDiameter, float normalizedTime) {
    if (linePath == null) {
      return;
    }
    int index = 0;
    float adjustNormalizedTime = 0;
    if (normalizedTime <= CIRCLE_TO_LINE_SEASONS[1]) {
      adjustNormalizedTime = normalizedTime / CIRCLE_TO_LINE_SEASONS[1];
    } else if (normalizedTime < CIRCLE_TO_LINE_SEASONS[2]) {
      index = 1;
      adjustNormalizedTime = (normalizedTime - CIRCLE_TO_LINE_SEASONS[1])
          / (CIRCLE_TO_LINE_SEASONS[2] - CIRCLE_TO_LINE_SEASONS[1]);
    } else {
      index = 2;
      adjustNormalizedTime = (normalizedTime - CIRCLE_TO_LINE_SEASONS[2])
          / (CIRCLE_TO_LINE_SEASONS[3] - CIRCLE_TO_LINE_SEASONS[2]);
    }

    // the path bounds width
    int boundWidth = (int) (((CIRCLE_TO_LINE_WIDTH_FACTOR[index + 1]
        - CIRCLE_TO_LINE_WIDTH_FACTOR[index])
        * adjustNormalizedTime + CIRCLE_TO_LINE_WIDTH_FACTOR[index]) * circleDiameter);

    // the distance of cubic line1' x1 to cubic line2's x2
    int adjustBoundWidth = boundWidth;
    if (normalizedTime <= CIRCLE_TO_LINE_SEASONS[1]) {
      adjustBoundWidth = (int) (boundWidth * adjustNormalizedTime);
    }

    // the path bounds height
    int boundHeight = (int) (((CIRCLE_TO_LINE_HEIGHT_FACTOR[index + 1]
        - CIRCLE_TO_LINE_HEIGHT_FACTOR[index])
        * adjustNormalizedTime + CIRCLE_TO_LINE_HEIGHT_FACTOR[index]) * circleDiameter);

    // calculate the four points
    float firstControlXFactor = (CIRCLE_TO_LINE_FST_CON_X_FACTOR[index + 1]
        - CIRCLE_TO_LINE_FST_CON_X_FACTOR[index])
        * adjustNormalizedTime + CIRCLE_TO_LINE_FST_CON_X_FACTOR[index];
    float firstControlYFactor = (CIRCLE_TO_LINE_FST_CON_Y_FACTOR[index + 1]
        - CIRCLE_TO_LINE_FST_CON_Y_FACTOR[index])
        * adjustNormalizedTime + CIRCLE_TO_LINE_FST_CON_Y_FACTOR[index];
    float secondControlXFactor = (CIRCLE_TO_LINE_SEC_CON_X_FACTOR[index + 1]
        - CIRCLE_TO_LINE_SEC_CON_X_FACTOR[index])
        * adjustNormalizedTime + CIRCLE_TO_LINE_SEC_CON_X_FACTOR[index];
    float secondControlYFactor = (CIRCLE_TO_LINE_SEC_CON_Y_FACTOR[index + 1]
        - CIRCLE_TO_LINE_SEC_CON_Y_FACTOR[index])
        * adjustNormalizedTime + CIRCLE_TO_LINE_SEC_CON_Y_FACTOR[index];
    int firstControlX = (int) (circleDiameter * firstControlXFactor);
    int firstControlY = (int) (circleDiameter * firstControlYFactor);
    int secondControlX = (int) (circleDiameter * secondControlXFactor);
    int secondControlY = (int) (circleDiameter * secondControlYFactor);

    linePath.reset();
    // left line
    linePath.cubicTo(firstControlX, firstControlY,
        secondControlX, secondControlY, adjustBoundWidth / 2, boundHeight);
    // left right line
    linePath.cubicTo(adjustBoundWidth - secondControlX,
        secondControlY, adjustBoundWidth - firstControlX, firstControlY, adjustBoundWidth, 0);

    // translate path to move the origin to the center
    int offsetX = (circleDiameter - adjustBoundWidth) / 2;
    int offsetY = (circleDiameter - boundHeight) / 2;
    linePath.offset(offsetX, offsetY);
  }

  private void drawCircleAndArrowScale(Canvas canvas, float scaleFactor) {
    // draw bg circle
    canvas.save();
    canvas.scale(scaleFactor, scaleFactor, mCircleRectF.getCenter().getPointX(),
        mCircleRectF.getCenter().getPointY());
    mDefaultPaint.setColor(mLoadingCircleBackColor);
    canvas.drawCircle(mCircleRectF.getCenter().getPointX(), mCircleRectF.getCenter().getPointY(),
        mCircleRadius, mDefaultPaint);

    // draw arrow
    updateArrowPath(mInitArrowRectWidth,
        mInitArrowRectHeight, mInitArrowTriWidth, mInitArrowTriHeight);
    // translate the canvas  causes the center point of the arrow to coincide with the center
    // point of the circle
    canvas.translate(mCircleRectF.getCenter().getPointX() - mArrowRectF.getWidth() / 2 * scaleFactor,
        mCircleRectF.getCenter().getPointY() - mArrowRectF.getHeight() / 2 * scaleFactor);
    mDefaultPaint.setColor(mArrowColor);
    canvas.drawPath(mArrowPath, mDefaultPaint);
    canvas.restore();
  }

  private void drawCircleAndArrowScaleAndInnerScale(Canvas canvas, float scalingFactor,
                                                    float innerCircleScalingFactor) {
    // draw bg circle
    canvas.save();
    canvas.scale(scalingFactor, scalingFactor, mCircleRectF.getCenter().getPointX(),
        mCircleRectF.getCenter().getPointY());
    int layoutCont = canvas.saveLayer(mCircleRectF, mDefaultPaint);
    mDefaultPaint.setColor(mLoadingCircleBackColor);
    canvas.drawCircle(mCircleRectF.getCenter().getPointX(), mCircleRectF.getCenter().getPointY(),
        mCircleRadius, mDefaultPaint);

//        mDefaultPaint.setXfermode(mXfermode);
    // draw bg circle 2
    int innerCircleRadius = (int) (mCircleRadius * innerCircleScalingFactor);
    canvas.drawCircle(mCircleRectF.getCenter().getPointX(), mCircleRectF.getCenter().getPointY(),
        innerCircleRadius, mDefaultPaint);
//        mDefaultPaint.setXfermode(null);
    canvas.restoreToCount(layoutCont);

    // draw arrow
    updateArrowPath(mInitArrowRectWidth,
        mInitArrowRectHeight, mInitArrowTriWidth, mInitArrowTriHeight);
    // translate the canvas  causes the center point of the arrow to coincide with the center
    // point of the circle
    canvas.translate(mCircleRectF.getCenter().getPointX() - mArrowRectF.getWidth() / 2 * scalingFactor,
        mCircleRectF.getCenter().getPointY() - mArrowRectF.getHeight() / 2 * scalingFactor);
    mDefaultPaint.setColor(mArrowColor);
    canvas.drawPath(mArrowPath, mDefaultPaint);
    canvas.restore();
  }

  private void drawArrowTrans(Canvas canvas, int offsetX, int offsetY, float rotateAngle) {
    if (mArrowPath == null) {
      mArrowPath = new Path();
      updateArrowPath(mInitArrowRectWidth, mInitArrowRectHeight,
          mInitArrowTriWidth, mLastArrowTriWidth);
    }

    if (mArrowRectF == null) {
      mArrowRectF = new RectFloat();
      mArrowPath.computeBounds(mArrowRectF);
    }
    if (mArrowRotateMatrix == null) {
      mArrowRotateMatrix = new Matrix();
    } else {
      mArrowRotateMatrix.reset();
    }

    mDefaultPaint.setColor(mArrowColor);
    canvas.save();
    canvas.translate(offsetX, offsetY);
    // arrow shake
    if (rotateAngle != 0) {
      mArrowRotateMatrix.postRotate(rotateAngle,
          mArrowRectF.getCenter().getPointX(), mArrowRectF.bottom);
      canvas.concat(mArrowRotateMatrix);
    }
    canvas.drawPath(mArrowPath, mDefaultPaint);
    canvas.restore();
  }

  private void drawArrowTrans(Canvas canvas, int offsetX, int offsetY, float[] paramters) {
    updateArrowPath((int) (mCircleRadius * paramters[0]),
        (int) (mCircleRadius * paramters[1]),
        (int) (mCircleRadius * paramters[2]),
        (int) (mCircleRadius * paramters[3]));
    drawArrowTrans(canvas, offsetX, offsetY, paramters[4]);
  }

  /**
   * @param oscillFactor            line oscillation factor
   * @param baseLineX               The the original X coordinate of starting point of the base
   *                                straight line
   * @param baseLineY               The the original Y coordinate of starting point of the base
   *                                straight line
   * @param highestPointHeight      The height highest point
   * @param highestPointPaddingLeft Indicates the distance from the top of the rope to the left
   *                                of the rope
   */
  private void updateLineOscillationPath(
      float oscillFactor, int baselineLen,
      int baseLineX, int baseLineY, int highestPointHeight, int highestPointPaddingLeft) {
    if (mOscillationLinePath == null) {
      mOscillationLinePath = new Path();
    } else {
      mOscillationLinePath.reset();
    }
    // calculate the y of the control point
    highestPointHeight *= oscillFactor;
    int controlPointY = (int) (highestPointHeight * baselineLen
        * baselineLen / (2f * highestPointPaddingLeft * (baselineLen - highestPointPaddingLeft)));

    // move to start point
    mOscillationLinePath.moveTo(baseLineX, baseLineY);
    mOscillationLinePath.quadTo(baseLineX + highestPointPaddingLeft,
        baseLineY - controlPointY,
        baseLineX + baselineLen, baseLineY);
  }

  private void drawProgressLinePath(
      Canvas canvas, float normalizeProgress, int baselineLen,
      int baseLineX, int baseLineY, int highestPointHeight, Color leftLineColor) {
    int halfLen = baselineLen / 2;
    int middlePointX = (int) (baseLineX + baselineLen * normalizeProgress);
    int middlePointY;

    float k = (float) highestPointHeight / halfLen;
    if (normalizeProgress < HALF_NORMALIZED_PROGRESS) {
      middlePointY = (int) (halfLen * k
          * normalizeProgress / HALF_NORMALIZED_PROGRESS) + baseLineY;
    } else {
      middlePointY = (int) (halfLen * k
          * (1 - normalizeProgress) / HALF_NORMALIZED_PROGRESS) + baseLineY;
    }
    // draw right part first
    mBaseLinePaint.setColor(mLoadingLineColor);
    canvas.drawLine(middlePointX, middlePointY, baseLineX + baselineLen,
        baseLineY, mBaseLinePaint);

    // draw left part
    mBaseLinePaint.setColor(leftLineColor);
    canvas.drawLine(baseLineX, baseLineY, middlePointX, middlePointY, mBaseLinePaint);
    if (mProgressLinePathRectF == null) {
      mProgressLinePathRectF = new RectFloat();
    }
    mProgressLinePathRectF.modify(baseLineX, baseLineY, baseLineX + baselineLen, middlePointY);
  }

  public void performAnimation() {
    mIsFailed = false;
    mLastProgress = ZERO_PROGRESS;
    mNextProgress = ZERO_PROGRESS;
    releaseAnimation();

    // circle and arrow scale
    mBefProgCircleScaleAnimator = new AnimatorValue();
    mBefProgCircleScaleAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mBefProgCircleScalingFactor = AnimatorValueUtils.getAnimatedValue(v, 1f, 0.91f, 1f);
        invalidate();
      }
    });
    mBefProgCircleScaleAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_BEFORE_PROGRESS_CIRCLE_SCALE;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });

    mBefProgCircleScaleAnimator.setDuration(BEFORE_PROGRESS_CIRCLE_SCALE_DURATION);
    mBefProgCircleScaleAnimator.setDelay(BEFORE_PROGRESS_INNER_CIRCLE_SCALE_DELAY);
    mAnimatorList.add(mBefProgCircleScaleAnimator);
    // inner circle arrow scale
    mBefProgInnerCircleScaleAnimator = new AnimatorValue();
    mBefProgInnerCircleScaleAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mBefProgInnerCircleScalingFactor = AnimatorValueUtils.getAnimatedValue(v, 0, 0.9f);
        invalidate();
      }
    });
    mBefProgInnerCircleScaleAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_BEFORE_PROGRESS_INNER_CIRCLE_SCALE;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });

    mBefProgInnerCircleScaleAnimator.setDuration(BEFORE_PROGRESS_INNER_CIRCLE_SCALE_DURATION);
    mBefProgInnerCircleScaleAnimator.setDelay(BEFORE_PROGRESS_INNER_CIRCLE_SCALE_DELAY);
    mAnimatorList.add(mBefProgInnerCircleScaleAnimator);

    // circle to line animation
    mBefProgCircleToLineAnimator = new AnimatorValue();
    mBefProgCircleToLineAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mBefProgCircleToLineNormalizedTime = AnimatorValueUtils.getAnimatedValue(v, 0, 1f);
        invalidate();
      }
    });
    mBefProgCircleToLineAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_BEFORE_PROGRESS_CIRCLE_TO_LINE;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });

    mBefProgCircleToLineAnimator.setDuration(BEFORE_PROGRESS_CIRCLE_TO_LINE_DURATION);
    mAnimatorList.add(mBefProgCircleToLineAnimator);

    // arrow move animation
    mBefProgArrowMoveAnimator = new AnimatorValue();
    mBefProgArrowMoveAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mBefProgArrowMoveNormalizedTime = AnimatorValueUtils.getAnimatedValue(v, 0, 1f);
        invalidate();
      }
    });
    mBefProgArrowMoveAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_BEFORE_PROGRESS_ARROW_MOVE_LINE_OSCILL;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {
        performProgressAnimation();
      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });

    mBefProgArrowMoveAnimator.setDuration(BEFORE_PROGRESS_ARROW_MOVE_AND_LINE_OSCILL);
    mAnimatorList.add(mBefProgArrowMoveAnimator);

    // line oscill animation
    mBefProgLineOscillAnimator = new AnimatorValue();
    mBefProgLineOscillAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mBefProgLineOscillationFactor = AnimatorValueUtils.getAnimatedValue(v, 0, 1, -0.5f, 0.25f
            , -0.125f, 0);
      }
    });
    mBefProgLineOscillAnimator.setDuration(BEFORE_PROGRESS_ARROW_MOVE_AND_LINE_OSCILL);
    AnimatorGroup arrowMoveAndLineOscillSet = new AnimatorGroup();
    arrowMoveAndLineOscillSet.runParallel(mBefProgArrowMoveAnimator, mBefProgLineOscillAnimator);
    mAnimatorList.add(arrowMoveAndLineOscillSet);

    mBefProgressAnimatorSet = new AnimatorGroup();
    mBefProgressAnimatorSet.runSerially(mBefProgCircleScaleAnimator,
        mBefProgInnerCircleScaleAnimator,
        mBefProgCircleToLineAnimator, arrowMoveAndLineOscillSet);
//        mBefProgressAnimatorSet.runParallel(mBefProgInnerCircleScaleAnimator);
    mBefProgressAnimatorSet.setCurveType(Animator.CurveType.CUBIC_BEZIER_DECELERATION);
    mAnimatorList.add(mBefProgressAnimatorSet);
    mBefProgressAnimatorSet.start();
  }

  private void performProgressAnimation() {
    int startProgress = mLastProgress;
    int endProgress = mNextProgress;
    mLastProgress = mNextProgress;

    if (mIsFailed) {
      performFailedAnimation();
      return;
    }

    mProgressAnimator = new AnimatorValue();
    mProgressAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mProgressValue = AnimatorValueUtils.getAnimatedValue(v, startProgress, endProgress);
        invalidate();
      }
    });
    mProgressAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_IN_PROGRESS;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {
        if (mIsFailed) {
          performFailedAnimation();
        } else if (mLastProgress != FULL_PROGRESS) {
          performProgressAnimation();
        } else {
          performDoneAnimation();
        }
      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });
    mProgressAnimator.setCurveType(Animator.CurveType.CUBIC_BEZIER_DECELERATION);
    mProgressAnimator.setDuration(Math.max(
        FULL_PROGRESS_ANIMATION_DURATION
            * (endProgress - startProgress) / FULL_PROGRESS,
        MIN_PROGRESS_ANIMATION_DURATION));
    mAnimatorList.add(mProgressAnimator);
    mProgressAnimator.start();
  }

  private void performDoneAnimation() {
    // done rotate animation
    mDoneRotateAnimator = new AnimatorValue();
    mDoneRotateAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mDoneRotateNormalizedTime = AnimatorValueUtils.getAnimatedValue(v, 0, 1);
        invalidate();
      }
    });
    mDoneRotateAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_DONE_ROTATE;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });

    mDoneRotateAnimator.setDuration(DONE_ANIMATION_DURATION);
    mAnimatorList.add(mDoneRotateAnimator);

    // line pack up animation
    mDoneLinePackUpAnimator = new AnimatorValue();
    mDoneLinePackUpAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mDoneLinePackUpNormalizedTime = AnimatorValueUtils.getAnimatedValue(v, 0, 1);
        invalidate();
      }
    });
    mDoneLinePackUpAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_DONE_LINE_PACK_UP;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });
    mDoneLinePackUpAnimator.setDuration(DONE_LINE_PACK_UP_ANIMATION_DURATION);
    mAnimatorList.add(mDoneLinePackUpAnimator);

    // arrow shake animator and line change to a point
    mDoneArrowShakeAnimator = new AnimatorValue();
    mDoneArrowShakeAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mDoneArrowShakeAngle = AnimatorValueUtils.getAnimatedValue(v, 0, -20, 20, -10, 10);
        invalidate();
      }
    });
    mDoneArrowShakeAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_DONE_ARROW_SHAKE;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });
    mDoneArrowShakeAnimator.setDuration(DONE_LINE_PACK_UP_ARROW_SHAKE_ANIMATION_DURATION);
    mAnimatorList.add(mDoneArrowShakeAnimator);

    // circle scale animation
    mDoneRestToCircleAnimator = new AnimatorValue();

    mDoneRestToCircleAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mDoneRestToCircleScalingFactor = AnimatorValueUtils.getAnimatedValue(v, 0f, 1.1f, .9f, 1f);
        invalidate();
      }
    });
    mDoneRestToCircleAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_DONE_REST_TO_CIRCLE;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });
    mDoneRestToCircleAnimator.setDuration(DONE_REST_TO_CIRCLE_DOWN_ANIMATION_DURATION);
    mAnimatorList.add(mDoneRestToCircleAnimator);

    // dialog change to arrow
    mDoneDialogToArrowAnimator = new AnimatorValue();
    mDoneDialogToArrowAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mDoneDialogToArrowNormalizedTime = AnimatorValueUtils.getAnimatedValue(v, 0, 1f);
      }
    });
    mDoneDialogToArrowAnimator.setDuration(DONE_DIALOG_TO_ARROW_DURATION);
    mAnimatorList.add(mDoneDialogToArrowAnimator);
    mDoneDialogToArrowAnimator.start();

    // arrow up and down animation
    mDoneDialogToArrowUpDownAnimator = new AnimatorValue();

    mDoneDialogToArrowUpDownAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mDoneDialogToArrowUpDownFactor = AnimatorValueUtils.getAnimatedValue(v, 0f, -0.2f, 0.1f,
            -0.05f, 0f);
      }
    });
    mDoneDialogToArrowUpDownAnimator.setDuration(DONE_DIALOG_UP_DOWN_DURATION);
    mAnimatorList.add(mDoneDialogToArrowUpDownAnimator);

    AnimatorGroup dialogToArrowAnimatorSet = new AnimatorGroup();
    dialogToArrowAnimatorSet.runSerially(mDoneDialogToArrowAnimator,
        mDoneDialogToArrowUpDownAnimator);
    mAnimatorList.add(dialogToArrowAnimatorSet);

    AnimatorGroup restToCircleAnimatorSet = new AnimatorGroup();
    restToCircleAnimatorSet.runParallel(mDoneRestToCircleAnimator, dialogToArrowAnimatorSet);

    mAnimatorList.add(restToCircleAnimatorSet);

    // done animator set
    mDoneAnimatorSet = new AnimatorGroup();
    mDoneAnimatorSet.setCurveType(Animator.CurveType.CUBIC_BEZIER_DECELERATION);
    mDoneAnimatorSet.runSerially(mDoneRotateAnimator,
        mDoneLinePackUpAnimator, mDoneArrowShakeAnimator, restToCircleAnimatorSet);
    mAnimatorList.add(mDoneAnimatorSet);
    mDoneAnimatorSet.start();
  }

  private void performFailedAnimation() {
    // failed arrow shake animation
    mFailedArrowShakeAnimator = new AnimatorValue();
    mFailedArrowShakeAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mFailedArrowUpAndDownFactor = AnimatorValueUtils.getAnimatedValue(v, 0, 0.05f, -0.05f,
            0.1f, -0.15f, 0.25f, -1f);
        invalidate();
      }
    });
    mFailedArrowShakeAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_FAILED_ARROW_SHAKE;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });
    mFailedArrowShakeAnimator.setDuration(FAILED_ANIMATION_DURATION);
    mAnimatorList.add(mFailedArrowShakeAnimator);

    // failed arrow rotate animation
    mFailedArrowRotateAnimator = new AnimatorValue();
    mFailedArrowRotateAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mFailedArrowRotateAngle = AnimatorValueUtils.getAnimatedValue(v, 0, 0.5f, -0.5f, 0.25f,
            0.25f, 0f);
      }
    });
    mFailedArrowRotateAnimator.setDuration(FAILED_ANIMATION_DURATION);
    mAnimatorList.add(mFailedArrowRotateAnimator);

    // failed bomb animator
    mFailedBombAnimator = new AnimatorValue();
    mFailedBombAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mFailedBombAnimatorPer = AnimatorValueUtils.getAnimatedValue(v, 0, 1f);
      }
    });
    mFailedBombAnimator.setDuration(FAILED_BOMB_ANIMATION_DURATION);
    mAnimatorList.add(mFailedBombAnimator);

    // arrow shake rotate and line bomb animatorSet
    AnimatorGroup arrowShakeRotateAndLineBomb = new AnimatorGroup();
    arrowShakeRotateAndLineBomb.runParallel(
        mFailedArrowShakeAnimator, mFailedArrowRotateAnimator, mFailedBombAnimator);
    mAnimatorList.add(arrowShakeRotateAndLineBomb);

    // failed line oscillation animation
    mFailedLineOscillationAnimator = new AnimatorValue();
    mFailedLineOscillationAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mFailedLineOscillationFactor = AnimatorValueUtils.getAnimatedValue(v, 0, 0.2F, -0.2F,
            0.1F, -0.1F, 0F);
      }
    });
    mFailedLineOscillationAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_FAILED_LINE_OSCILL;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });
    mFailedLineOscillationAnimator.setDuration(FAILED_ROPE_OSCILLATION_ANIMATION_DURATION);
    mAnimatorList.add(mFailedLineOscillationAnimator);

    // failed line pack up Animator
    mFailedLinePackUpAnimator = new AnimatorValue();
    mFailedLinePackUpAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mFailedLinePackUpFactor = AnimatorValueUtils.getAnimatedValue(v, 1, 0f);
      }
    });
    mFailedLinePackUpAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_FAILED_LINE_PACK_UP;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });
    mFailedLinePackUpAnimator.setDuration(FAILED_ROPE_PACK_UP_ANIMATION_DURATION);
    mAnimatorList.add(mFailedLinePackUpAnimator);

    // failed circle scale animation
    mFailedCircleScaleAnimator = new AnimatorValue();
    mFailedCircleScaleAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mFailedCircleScaleFactor = AnimatorValueUtils.getAnimatedValue(v, 0, 1.1f, 0.9f, 1.0f);
      }
    });
    mFailedCircleScaleAnimator.setStateChangedListener(new Animator.StateChangedListener() {
      @Override
      public void onStart(Animator animator) {
        mCurrentState = STATE_FAILED_CIRCLE_SCALE;
      }

      @Override
      public void onStop(Animator animator) {

      }

      @Override
      public void onCancel(Animator animator) {

      }

      @Override
      public void onEnd(Animator animator) {

      }

      @Override
      public void onPause(Animator animator) {

      }

      @Override
      public void onResume(Animator animator) {

      }
    });
    mFailedCircleScaleAnimator.setCurveType(Animator.CurveType.PHYSICAL_FLING);
    mFailedCircleScaleAnimator.setDuration(FAILED_CIRCLE_SCALE_ANIMATION_DURATION);
    mAnimatorList.add(mFailedCircleScaleAnimator);

    AnimatorGroup lineOscillPackAndCircleScaleSet = new AnimatorGroup();
    lineOscillPackAndCircleScaleSet.runSerially(
        mFailedLineOscillationAnimator, mFailedLinePackUpAnimator, mFailedCircleScaleAnimator);
    mAnimatorList.add(lineOscillPackAndCircleScaleSet);

    // failed arrow move animation
    mFailedArrowMoveAnimator = new AnimatorValue();
    mFailedArrowMoveAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
      @Override
      public void onUpdate(AnimatorValue animatorValue, float v) {
        mFailedArrowMoveNormalizeTiem = AnimatorValueUtils.getAnimatedValue(v, 0, .2f, .4f, .6f, .8f, 1f, .95f, 1f);
        invalidate();

      }
    });
    mFailedArrowMoveAnimator.setDuration(FAILED_ARROW_MOVE_ANIMATION_DURATION);
    mAnimatorList.add(mFailedArrowMoveAnimator);


    AnimatorGroup lineOscillAndArrowMoveSet = new AnimatorGroup();
    lineOscillAndArrowMoveSet.runParallel(lineOscillPackAndCircleScaleSet, mFailedArrowMoveAnimator);
    mAnimatorList.add(lineOscillAndArrowMoveSet);

    mFailedAnimatorSet = new AnimatorGroup();
    mFailedAnimatorSet.setCurveType(Animator.CurveType.SPRING);
    mFailedAnimatorSet.runSerially(arrowShakeRotateAndLineBomb, lineOscillAndArrowMoveSet);
    mAnimatorList.add(mFailedAnimatorSet);
    mFailedAnimatorSet.start();

  }

  public void releaseAnimation() {
    if (mAnimatorList == null || mAnimatorList.size() == 0) {
      return;
    }

    for (Animator animator : mAnimatorList) {
      if (animator == null) {
        continue;
      }

      if (animator instanceof AnimatorValue) {
        ((AnimatorValue) animator).stop();
        animator.stop();
      }
      animator.stop();
      animator.cancel();
      animator = null;
    }
    mAnimatorList.clear();
  }

  public void updateProgress(int progress) {
    // adjust progress in area 0 to 100
    if (progress < ZERO_PROGRESS) {
      progress = ZERO_PROGRESS;
    } else if (progress > FULL_PROGRESS) {
      progress = FULL_PROGRESS;
    }
    mNextProgress = Math.max(progress, mNextProgress);
  }

  public void onFail() {
    mIsFailed = true;
  }

  private int dipToPx(Context context, int dip) {
    return (int) (dip * getScreenDensity(context) + 0.5f);
  }

  private float getScreenDensity(Context context) {
    try {
      // 获取屏幕密度
      Optional<Display>
          display = DisplayManager.getInstance().getDefaultDisplay(context);
      DisplayAttributes displayAttributes = display.get().getAttributes();
      return displayAttributes.densityPixels;
    } catch (Exception e) {
      return 160;
    }
  }
}
